/**
 * XSSProtectProcessor.java
 * com.sftz.framework.processor
 * Copyright (c) 2018, 北京科技有限公司版权所有.
*/

package com.sftz.framework.mvc.processor;

import static com.uxuexi.core.common.util.Util.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.nutz.lang.Mirror;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.mvc.ActionContext;
import org.nutz.mvc.annotation.Param;
import org.nutz.mvc.impl.processor.AbstractProcessor;
import org.nutz.mvc.upload.FieldMeta;

import com.sftz.framework.mvc.annotation.XssParam;
import com.sftz.framework.util.XssFilterUtil;
import com.uxuexi.core.common.util.Util;

/**
 * 防跨站脚本攻击过滤器</br>
 * <p>
 * 注意:
 * 1，只有含有富文本注解的POJO属性会采用富文本的方式进行过滤；
 * 2，其他参数全部转为纯文本。
 * 3，不要采用Map类型传递参数
 * 4，不要定义Object类型的属性来接收参数
 * @author   朱晓川
 * @Date	 2018年3月5日 	 
 */
public class XSSProtectProcessor extends AbstractProcessor {

	private static final Log log = Logs.get();

	@Override
	public void process(ActionContext ac) throws Throwable {
		//添加安全响应头信息
		HttpServletResponse response = ac.getResponse();
		if (!Util.isEmpty(response)) {
			/*
			 假如请求类型为以下两种，那么阻止请求的发生：
				"style" 但是 MIME 类型不是 "text/css"，
				"script" 但是 MIME 类型不是  JavaScript MIME 类型。
			 */
			response.setHeader("X-Content-Type-Options", "nosniff");

			//用户浏览器发现xss攻击的时候不加载页面
			response.setHeader("X-XSS-Protection", "1;mode=block");

			//该页面可以在相同域名页面的 frame 中展示
			response.setHeader("X-Frame-Options", "SAMEORIGIN");

		}

		//获取到入口函数的所有参数
		Object[] args = ac.getMethodArgs();
		if (isEmpty(args)) {
			doNext(ac);
			return;
		}

		int index = 0;
		for (int foot = 0; foot < args.length; foot++) {
			Method method = ac.getMethod();
			Annotation[] annotations = method.getParameterAnnotations()[index++];

			Object arg = args[foot];
			//空值-_-...
			if (isEmpty(arg)) {
				continue;
			}
			Mirror<Object> me = Mirror.me(arg);
			Class<? extends Object> argType = arg.getClass();

			//参数注解(查找@Param)
			if (isEmpty(annotations)) {
				if (me.isPrimitiveNumber() || ServletResponse.class.isAssignableFrom(argType)
						|| HttpSession.class.isAssignableFrom(argType)) {
					//do nothing
				} else if (ServletRequest.class.isAssignableFrom(argType)) {
					log.warn("入口参数使用了HttpServletRequest.请检查用途,确保没有xss漏洞.访问路径:" + ac.getRequest().getServletPath());
				} else {
					String statError = "入口参数没有@Param注解时,只能使用[数值|HttpSession|HttpServletResponse|HttpServletRequest].访问路径:"
							+ ac.getRequest().getServletPath();
					log.debug(statError);
					throw new IllegalStateException(statError);
				}
				continue;
			}
			for (Annotation annotation : annotations) {
				if (annotation instanceof Param) {
					//当前参数有@Param注解
					if (ServletRequest.class.isAssignableFrom(argType)
							|| ServletResponse.class.isAssignableFrom(argType)
							|| HttpSession.class.isAssignableFrom(argType)) {
						String statError = "请不要在入口参数:HttpSession|HttpServletResponse|HttpServletRequest上添加@Param注解!!!访问路径:"
								+ ac.getRequest().getServletPath();
						log.debug(statError);
						throw new IllegalStateException(statError);
					}

					filterEntranceParam(args, arg, foot, me);
				}
			}
		}
		doNext(ac);
	}

	/**
	 * 过滤入口方法的参数
	 * <p>
	 *
	 * @param args  入口方法的所有参数
	 * @param arg   当前要过滤的参数
	 * @param foot  当前正在过滤的参数在所有参数中的下标
	 * @param argMirror  当前参数的Mirror
	 */
	private void filterEntranceParam(Object[] args, Object arg, int foot, Mirror<Object> argMirror)
			throws IllegalArgumentException, IllegalAccessException, InstantiationException {

		//1,数值类型
		if (argMirror.isPrimitiveNumber()) {
			//不需要xss检查
		}

		//2,String，则使用纯文本过滤
		else if (argMirror.isString()) {
			log.debug("xss过滤,入口字符串:" + arg);
			String safe = pureText((String) arg);
			args[foot] = safe;//替换成安全的值
			log.debug("xss过滤,入口字符串safe:" + safe);
		}

		//3,数组
		else if (argMirror.isArray()) {
			Object[] arr = (Object[]) arg;
			if (!Util.isEmpty(arr)) {
				for (int i = 0; i < arr.length; i++) {
					Object obj = arr[i];
					Mirror<Object> mobj = Mirror.me(obj);
					//pojo 元素
					if (mobj.isPojo()) {
						filterPojoXss(obj, mobj);
					}

					//字符串
					else if (mobj.isString()) {
						log.debug("xss过滤,入口数组:" + obj);
						String safe = pureText((String) obj);
						log.debug("xss过滤,入口数组safe:" + safe);
						arr[i] = safe;
					}

					else if (mobj.isMap()) {
						String statError = "入口参数禁止使用Map数组!!!";
						log.debug(statError);
						throw new IllegalStateException(statError);
					}

					else if (mobj.isArray()) {
						String statError = "入口参数禁止使用嵌套数组!!!";
						log.debug(statError);
						throw new IllegalStateException(statError);
					}

					else if (mobj.isCollection()) {
						String statError = "入口参数禁止使用集合数组!!!";
						log.debug(statError);
						throw new IllegalStateException(statError);
					}
				}
			}
		}

		//4,POJO
		else if (argMirror.isPojo()) {
			filterPojoXss(arg, argMirror);
		}

		//5,其他
		else {
			String statError = "不支持的参数传递方式，请替换为[简单数值类型|字符串|数组|POJO]";
			log.debug(statError);
			throw new IllegalStateException(statError);
		}
	}

	/**
	 * 过滤Pojo类型的参数
	 * <p>
	 * 递归处理POJO的字符串属性(包括数组和集合) 
	 */
	private void filterPojoXss(Object pojo, Mirror<?> pojoMe) throws IllegalArgumentException, IllegalAccessException,
			InstantiationException {

		if (Util.isEmpty(pojo) || Util.isEmpty(pojoMe)) {
			//不理会空对象
			return;
		}

		//获取POJO对象的所有属性
		Field[] fields = pojoMe.getFields();
		if (!Util.isEmpty(fields)) {
			for (Field f : fields) {
				f.setAccessible(true);
				Object fval = f.get(pojo);
				if (Util.isEmpty(fval)) {
					//不理会空值
					continue;
				}

				Mirror<Object> mf = Mirror.me(fval);
				//1,带有@see XssParam 的String字段，进行xss过滤，否则转为纯文本
				if (mf.isString()) {

					XssParam ann = f.getAnnotation(XssParam.class);
					if (Util.isEmpty(ann)) {
						log.debug("xss过滤,POJO-字符串属性:" + f.getName() + ",原值:" + fval);
						String safe = XssFilterUtil.pureText((String) fval);
						log.debug("xss过滤,POJO-字符串属性:" + f.getName() + ",safe值:" + pojoMe.getValue(pojo, f));
						pojoMe.setValue(pojo, f, safe);

					} else {
						//只有Pojo传递参数加了XssParam才会使用富文本过滤
						log.debug("xss过滤,POJO-富文本属性:" + f.getName() + ",原值:" + fval);
						String safe = XssFilterUtil.relaxed((String) fval);
						log.debug("xss过滤,POJO-富文本属性:" + f.getName() + ",safe值:" + pojoMe.getValue(pojo, f));
						pojoMe.setValue(pojo, f, safe);
					}
				}

				//2,Collection
				else if (mf.isCollection()) {

					Collection<?> col = (Collection<?>) fval;
					Collection<Object> nw = col.getClass().newInstance();

					//遍历元素
					if (!Util.isEmpty(col)) {
						for (Object el : col) {
							Mirror<Object> mel = Mirror.me(el);

							//pojo 元素
							if (mel.isPojo()) {
								nw.add(el);
								filterPojoXss(el, mel);
							}

							//过滤字符串
							else if (mel.isString()) {
								log.debug("xss过滤,POJO-Collection<String>属性:" + f.getName() + ",原值:" + el);
								String safe = pureText((String) el);
								log.debug("xss过滤,POJO-Collection<String>属性:" + f.getName() + ",safe值:" + safe);
								nw.add(safe);
							}

							else if (mel.isMap()) {
								String statError = "form属性禁止使用Collection<Map>!!!";
								log.debug(statError);
								throw new IllegalStateException(statError);
							}

							else if (mel.isArray()) {
								String statError = "form属性禁止使用Collection<数组>!!!";
								log.debug(statError);
								throw new IllegalStateException(statError);
							}

							else if (mel.isCollection()) {
								String statError = "form属性禁止使用嵌套集合!!!";
								log.debug(statError);
								throw new IllegalStateException(statError);
							}

							//其他类型
							else {
								nw.add(el);
							}
						}
						pojoMe.setValue(pojo, f, nw);
					}
				}

				//3,数组
				else if (mf.isArray()) {
					// 转成数组
					Object[] arr = (Object[]) fval;
					if (!Util.isEmpty(arr)) {
						for (int i = 0; i < arr.length; i++) {
							Object obj = arr[i];
							Mirror<Object> mobj = Mirror.me(obj);
							//pojo 元素
							if (mobj.isPojo()) {
								filterPojoXss(obj, mobj);
							}

							//字符串
							else if (mobj.isString()) {
								log.debug("xss过滤,POJO-String数组属性:" + f.getName() + ",原值:" + obj);
								String safe = pureText((String) obj);
								log.debug("xss过滤,POJO-String数组属性:" + f.getName() + ",safe值:" + safe);
								arr[i] = safe;
							}
						}
					}
				}

				//4,POJO
				else if (mf.isPojo()) {
					filterPojoXss(fval, mf);
				}

				else if (mf.isMap()) {
					//只允许文件上传的时候TempFile对象中的map
					if (!pojoMe.is(FieldMeta.class)) {
						//请不要在POJO中使用Map类型接收参数
						String statError = "请不要在POJO中使用Map类型接收参数!!!";
						log.debug(statError);
						throw new IllegalStateException(statError);
					}
				}
			}
		}//end of fields if

	}

	/**纯文本过滤*/
	private String pureText(String val) {
		return XssFilterUtil.pureText(val);
	}
}